# Практика Avi
## Про `selectStatement` и `onRefreshExt`
Изначально результат выборки формируется операцией `onRefresh` или `onRefreshItem` одним из двух способов: реляционный или объектный запрос.
### Реляционный запрос
В этом случае в теле `onRefresh` вызывается метод `selectStatement` или `prepareSelectStatement` (вызывает тот же `selectStatement`, но с добавлением фильтров, описанных в `avm`).
Иначе говоря, результатом операции `onRefresh` является текстовое значение, внутри которого реляционный запрос.
```{note}
onRefreshExt в случае формирования выборки реляционным запросом не вызывается!
```
Так формируется по умолчанию отображение `List` (кроме коллекций).
Не учитываются значения хранящиеся в кэше, учитываются значения только из БД.
Пример `selectStatement` для отображения `List`:
```scala
override protected def selectStatement: String = {
s"""SELECT
t.id
,t.idClass
,t.idState
,t1.sHeadLine_dz as idStateHL
,t.idStateMC
,t.idObjectType
,t2.sHeadLine_dz as idObjectTypeHL
,t.idDepOwner
,t3.sHeadLine_dz as idDepOwnerHL
,t.gidSrc
,t.idResourceHolder
,t4.sHeadLine_dz as idResourceHolderHL
,t.sRegNum
,t.dReg
,t.idPeriod
,t5.sHeadLine_dz as idPeriodHL
,t.sDescription
,t.gid
,t.sRegNum_dz
,t.sRegNumBMs_dz
,t.sRegNumidVer_dz
FROM Oil_DemandMov t
LEFT JOIN Btk_ClassState t1 on t.idState = t1.id
LEFT JOIN Btk_ObjectType t2 on t.idObjectType = t2.id
LEFT JOIN Bs_DepOwner t3 on t.idDepOwner = t3.id
LEFT JOIN Bs_Contras t4 on t.idResourceHolder = t4.id
LEFT JOIN Bs_Period t5 on t.idPeriod = t5.id
"""
}
```
### Коммит в БД при выполнении реляционного запроса и @FlushBefore
Важно понимать, что при реляционном запросе сервер приложения делает коммит в БД (особенность работы сервера приложения). Поэтому на выборках, где вводятся значения в поля и поддерживается возможность пользователю отменить изменения, нельзя использовать формирование выборки на реляционном запросе. Иначе данные запишутся в БД без разрешения пользователя (без нажатия пользователем на операцию сохранения (дискетка)).
Однако, иногда есть необходимость в выполнении выборки на реляционном запросе в отображении с редактированием полей. Например, выборка выпадающего списка `Lookup`.
В таком случае перед `onRefresh` необходимо добавить аннотоцию `@FlushBefore(mode = FlushBeforeMode.Disabled)`:
```scala
trait Lookup extends Default with super.Lookup {
@FlushBefore(mode = FlushBeforeMode.Disabled)
override protected def onRefresh: Recs = {
s"""SELECT
t.id
,t.sHeadLine_dz as sHeadLine
,t.sMnemoCode_dz as sMnemoCode
,coalesce(t.sMnemoCode_dz, '') || ' ' || coalesce(t.sHeadLine_dz, '') as sMnemoCodeHeadLine
from AsfEqp_IntoOperExt t
order by upper(t.sHeadLine_dz)
"""
}
}
```
Иначе при открытии выпадающего списка данные будут записываться в БД.
## Объектный запрос
В этом случае onRefresh формируется из экземпляров объектов `SRop` (инструменты `refreshByParent`, `load`, `TxIndex`, `OQuery`) или `case class`’ов.
В этом случае учитываются значения, хранящиеся в кэше.
Примеры:
По умолчанию отображение `List` у коллекций формируется с помощью `refreshByParent`:
```scala
trait List_idPlanFactoryShip extends Default with super.List_Master {
override protected def onRefresh: Recs = {
Oil_PlanFactShipSegrGroupApi().refreshByParent(getIdMaster)
}
}
```
По умолчанию отображение `Card` у коллекций формируется с помощью `load`:
```scala
trait Card extends Default with super.Card {
override protected def onRefreshItem: Recs = {
Oil_PlanFactShipSegrGroupApi().load(getVar(getPKFieldName).asNLong)
}
}
```
### onRefreshExt
В случае объектного запроса сервер сам вызывает метод `onRefreshExt`, в который можно дописать получение нехранимых полей на синтаксисе `SQL`, НЕ учитывая значения из кэша:
```scala
override protected def onRefreshExt: String = {
s"""with t as ( select
:id as id
,:idPlanFactoryShip as idPlanFactoryShip
,:idSegregationGroup as idSegregationGroup
)
SELECT
t.id
,t1.sHeadLine_dz as idPlanFactoryShipHL
,t2.sHeadLine_dz as idSegregationGroupHL
,t2.sMnemoCode_dz as idSegregationGroupMC
FROM t
LEFT JOIN Oil_PlanFactoryShip t1 on t.idPlanFactoryShip = t1.id
LEFT JOIN Oil_SegregationGroup t2 on t.idSegregationGroup = t2.id
"""
}
```
```{note}
Также существует другой способ реализовать нехранимые поля (через case class AdditionalInfo), который будет описан дальше. Данный способ формирует значения с учётом кэша.
```
## Добавление нехранимых полей
Здесь будет рассмотрено 2 случая, как реализовать нехранимые поля в выборке, реализованные реляционным запросом и объектным.
Для формирования нехранимого поля нужно:
- получить поле в запросе под заданным псевдонимом
- описать поле в разметке `avm` под тем же псевдонимом
### Формировать реляционно или объектно
Если нехранимый атрибут вычисляется динамически от изменений на выборке, это значит, что работа идёт с значениями из кэша, т.е. с значениями, которые ещё не закоммичены в БД. Это означает, что нехранимое поле реализовывать нужно на объектном запросе.
Например: в карточке документа нужно посчитать сумму по всем записям в коллекции. Если поле суммы реализовать реляционно, то оно будет формироваться только из согласованных в БД значений. Это значит, для того чтобы учитывались новые записи в коллекции, их нужно закоммитить (сохранить) в БД.
Если же поле реализовать объектно, то в нём будут учитываться данные из кэша, т.е. те самые созданные новые записи в коллекции, которые ещё не закоммичены в БД.
### Нехранимые поля в реляционном запросе
```{note}
Не учитывает значения из кэша, которые не закоммичены в БД.
```
Чтобы добавить не хранимый атрибут реляционно, нужно иметь значение атрибута в выборке запроса `selectStatement`.
Примером может служить результат кодогенератора ссылочного поля для отображения `List`, где описывается получение значения хэдлайна в `Dvi` и описание атрибута в `dvm`.
Dvi:
```scala
override protected def selectStatement: String = {
s"""SELECT
t.id
,t.idClass
,t.idObjectType
,t1.sHeadLine_dz as idObjectTypeHL --нехранимое поле
,t.dReg
,t.sRegNum
,t.idState
,t2.sHeadLine_dz as idStateHL --нехранимое поле
FROM Rzd_Train t
LEFT JOIN Btk_ObjectType t1 on t.idObjectType = t1.id
LEFT JOIN Btk_ClassState t2 on t.idState = t2.id
"""
```
dvm:
```xml
```
```{note}
Напомню, что Dvi и dvm - это результат работы кодогенератора по данным описанным в разметке odm.
Разработчик работает в соответствующих файлах Avi и avm, переопределяя содержимое Dvi и dvm.
ВНИМАНИЕ! Изменять Dvi и dvm нельзя.
А точнее бесполезно, потому что изменения будут перетёрты при следующем запуске кодогенератора.
```
### Нехранимые поля в объектном запросе
```{note}
Учитывает значения из кэша, которые не закоммичены в БД.
```
#### onRefreshExt
Данный метод вызывается, если результатом `onRefresh` является набор объектов, а не текст, представляющий реляционный запрос.
С помощью `onRefreshExt` в отображении `Card` получены хэдлайны в `Dvi`:
```scala
override protected def onRefreshExt: String = {
s"""with t as ( select
:id as id
,:idObjectType as idObjectType
,:idState as idState
,:gidSrc/*@NString*/ as gidSrc
)
SELECT
t.id
,t1.sHeadLine_dz as idObjectTypeHL
,t2.sHeadLine_dz as idStateHL
,t3.sHeadLine as gidSrcHL
FROM t
LEFT JOIN Btk_ObjectType t1 on t.idObjectType = t1.id
LEFT JOIN Btk_ClassState t2 on t.idState = t2.id
LEFT JOIN Btk_Object t3 on t.gidSrc = t3.gidRef
"""
}
```
Здесь используется синтаксис postgresql.
Применяется конструкция with ([ссылка](https://www.postgresql.org/docs/current/queries-with.html)).
Через `":"` подставляются текущие значения атрибутов с выборки (в том числе из кэша). Так, например, `":id as id"` означает, что с выборки будет получено текущее значение атрибута `id` и подставлено в таблицу `t` под псевдонимом поля `id`. Теперь обращение в основном запросе `t.id` будет возвращать текущее значеине `id`.
```{note}
Под фразой "текущее значение" понимается значение на момент обновления выборки и выполнения метода onRefreshExt.
```
```{note}
Описание в dvm не меняется в зависимости от метода получения значения нехранимого поля (реляционного или объектного) и останется таким же, как в случае формирования нехранимого поля в selectStatement.
```
#### case class AdditionalInfo
Нехранимое поле можно сформировать, переопределив `onRefresh` и `onRefreshItem`.
В этом случае объект представляют как кортеж (`SRop`, экземпляр `case class’а`).
Тем самым объект имеет поля записи, описанных в БД (хранимые поля), и поля `case class’а` (нехранимые).
В этом случае принято называть `case class AdditionalInfo`, а заполнение описывать в методе `getAdditionalInfo`.
Нехранимое поле в отображении `Card`:
```scala
case class AdditionalInfo(
var nQtyLoadRecievActs: NNumber,
var nQtyLoadTransferCertificate: NNumber,
var nQtyLoadTransferCertificateAccounted: NNumber,
var nQtyRemains: NNumber,
var nQtyReserv: NNumber
)
protected def getAdditionalInfo(rop: Oil_ExternalMovApi#ApiRop): AdditionalInfo = {
AdditionalInfo(
nQtyLoadRecievActs = None.nn
, nQtyLoadTransferCertificate = None.nn
, nQtyLoadTransferCertificateAccounted = None.nn
, nQtyRemains = None.nn
, nQtyReserv = None.nn
)
}
override protected def onRefresh: Recs = {
val rop = thisApi().load(getVar(CardRep.IdItemSharp).asNLong)
(rop, getAdditionalInfo(rop))
}
override protected def onRefreshItem: Recs = {
val rop = thisApi().load(getVar(getPKFieldName).asNLong)
(rop, getAdditionalInfo(rop))
}
```
```{note}
Переменные внутри case class необходимо указывать var для Avi
```
Нехранимое поле в отображении `List_idBrigade` коллекции:
```scala
trait List_idBrigade extends Default with super.List_idBrigade {
case class AdditionalInfo(var sEmpId: NString = None.ns,
var sPosition: NString = None.ns)
def getAdditionalInfo(rop: Bs_BrigadeStaffApi#ApiRop): AdditionalInfo = {
if (rop.get(_.idEmployee).isNotNull) {
val avEmploee = Bs_EmployeeApi().load(rop.get(_.idEmployee)).copyAro()
AdditionalInfo(
sEmpId = avEmploee.sEmpId,
sPosition = avEmploee.sPosition)
} else AdditionalInfo()
}
override protected def onRefresh: Recs = {
Bs_BrigadeStaffApi().refreshByParent(getVarWithDep("super$id").asNLong)
.map(rop => {
(rop, getAdditionalInfo(rop))
})
}
}
```
Такой способ самый универсальный.
Например, с помощью `onRefreshExt` не получится посчитать сумму по всем записям коллекции с учётом кэша (для этого необходимо присоединить таблицу коллекции по условию `idparent = t.id`, что обеспечит учёт только тех записей и их значений, которые закоммичены в БД, т.е. без учёта кэша).
В рассматриваемой реализации, можно получить все записи коллекции с учётом кэша с помощью метода `refreshByParent(rop.get(_.id))` и далее в обходчике сложить необходимые значения полей.
## Нехранимые строки
Здесь будет описан пример реализации пустых предзаполненных строк в списке, как частный случай, в коллекции. Такие строки не хранятся в БД, а существуют визуально в интерфейсе (иными словами нехранимые строки). Запись в БД создаётся при вводе пользователем какого-либо поля.
Пример в проекте pgDev: `ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay`.
![](img/avi-emptyRow.jpg)
### `case class Row`
По сколку поля строки редактируемые, то не нужно использовать реляционный запрос для формирования выборки. Выборка будет формироваться экземплярами case class’а.
Таким образом onRefresh вернёт список экземпляров case class’а.
В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ.
Case class должен иметь такие же поля, как поля класса (таблицы), описанные в odm и нехранимые, формируемые не реляционно.
При добавлении нового хранимого поля, необходимо это поле добавить в case class.
```scala
/**
* Представление строки, поля соответствуют хранимым полям строки
*
* Полем id выступает idFlyOverPos, т.к. для работы некоторых инструментов (например, onRefreshExt) необходимо
* наличие уникального индификатора
*/
case class Row(
var id: NLong //Oil_RecievTaskDet.idFlyOverPos
, var gid: NGid = None.ng
, var idRecievTask: NLong
, var nRow: NNumber
, var idWagon: NLong = None.nl
, var idRailwayInvoice: NLong = None.nl
, var bUnloaded: NNumber = None.nn
, var bReFeed: NNumber = None.nn
, var idReFeedType: NLong = None.nl
, var bWagonRemoved: NNumber = None.nn
, var bRequiredMeasure: NNumber = None.nn
, var nQtyMeasure: NNumber = None.nn
, var idStorageTank: NLong = None.nl
, var sCertificateList: NString = None.ns
, var idRecievAct: NLong = None.nl
, var bRepeatedFeed: NNumber = None.nn
, var bCommAct: NNumber = None.nn
, var bRecievActCreated: NNumber = None.nn
, var idFlyOverPos: NLong = None.nl
, var idLockDeviceOut: NLong = None.nl
, var idRecievTaskDet: NLong = None.nl //Oil_RecievTaskDet.id
, var idTrain: NLong = None.nl
, var bRecievByMeasure: NNumber = None.nn
)
```
### `onRefresh`
`ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay#onRefresh`.
Описание алгоритма:
- Получаем все записи данной коллекции по мастеру (т.е. те, что хранятся в БД или в кэше).
```scala
RecievTaskDetApi().refreshByParent(getIdMaster)
```
- Получаем все вагоны выбранной у мастера «Пути эстакады».
```scala
val ropParent = Oil_RecievTaskApi().load(selection.master.getSelfVar("id").asNLong)
Oil_FlyOverPosApi().txidFlyOverWay.refreshByKey(ropParent.get(_.idFlyOverWay))
```
```{note}
В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ.
``````
- Реализуем обходчик по каждому вагону и заполняем экземпляры case class’а
- Если на данный вагон есть запись из таблицы БД, то заполняем данными из БД
- Иначе устанавливаем предзаполненные значения необходимых полей или `None`
```scala
ropaFOP.map(ropFOP => {
//поиск ропы в БД
val ropOpt = ropa.find(_.get(_.idFlyOverPos) === ropFOP.get(_.id))
getRowByRop(ropOpt, ropFOP, nvRow)
})
protected def getRowByRop(ropOpt: Option[SRop[_ <: JLong, _ <: Oil_RecievTaskDetAro]]
, ropFOP: Oil_FlyOverPosApi#ApiRop
, npRow: NNumber = None.nn
): Row = {
Row(
idPlacement = ropOpt.map(_.get(_.idPlacement)).nl
.nvl(thisApi().getidPlacementByFlyOverPos(ropFOP.get(_.id)))
...)
}
```
При добавлении нового хранимого атрибута, необходимо в этом методе заполнения описать правило заполнения нового поля.
На данном этапе предположим, что `id` заполняется от `ropOpt` (реальной записи), в случае отсутствия `None.nl`. Т.е. пустая строка имеет `id = None.nl`, что дальше будет использоваться, как признак пустой строки.
```{note}
Внимание: такая реализация не совсем корректна. В таком случае не будет работать ряд системных операций, таких, как onRefreshExt, которые требуют уникального значения id. Но для понимания будет рассмотрена такая реализация, а вопрос уникальности id будет рассмотрен далее.
```
`Row – case class`, представление строки, поля соответствуют хранимым полям записи.
- Возвращаем полученный список экземпляров case class’а.
### `insert` при вводе значения в нехранимую строку
Необходимо переопределить операцию `beforeEdit()`.
```scala
override def beforeEdit(): Unit = {
if (getSelfVar("id").isNull) {
regRow()
}
}
```
`beforeEdit()` вызывается при каждой попытке редактировать поле. Здесь необходимо инициализировать, что заполнение введётся в пустой нехранимой строке или в хранимой. Это можно сделать, проверив заполнен ли `id` (на данном этапе предполагается, что у пустой строки `id = None.nl`).
Если заполнение ведётся в пустой строке, то необходимо создать запись с переносом всех предустановленных значений и введённое пользователем значение применить к только что созданной записи.
Переносить предустановленные значения можно двумя способами:
- Прописать, какой сеттер вызывать и какое значение из case class’а подставлять на каждое поле в отдельности.
Такая реализация усложняет поддержание кода. При добавлении нового хранимого атрибута, в случае если оно имеет предустановленное значение, нужно добавлять код в этот фрагмент.
- Реализовать обходчик по всем имеющимся полям case class’a, отсечь поля, которые не имеют сеттеров (`gid`, например) и значение которых `null`, по оставшимся полям вызывать сеттеры и проставлять соответствующие значения:
```scala
/**
* insertByParent(ropParent) + сеттеры заполненных полей case class'а Row
*
* @param ropParent
* @param row
* @return
*/
def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = {
insertByParent(ropParent) :/ { rop =>
//setter'ы для переноса умолчательных значений из Row
row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to)
.filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull)
.foreach(field => {
setAttrValue(rop, field._1, field._2)
})
rop
}
}
```
Чтобы введенное пользователем значение относилось к только что созданной записи, необходимо принудительно после регистрации задать `id` на выборке:
```scala
val rop = thisApi().registerByRow(thisRow())
setVar("id", rop.get(_.id))
```
### `afterEdit()`
Также необходимо добавить проверку, является ли строка пустой, в `afterEdit()`.
Иначе `load()` внутри `afterEdit()` будет вызван по некорректного id пустой строки и будет вызвана ошибка.
### Поддержание уникального id в нехранимых полях.
Без уникального `id` ряд системных операций не будет работать или будет работать некорректно, в том числе для работы `onRefreshExt`.
Необходимо понять, что на выборке является уникальным, в случае примера это ссылка соответствующая вагону: `idFlyOverPos` .
Теперь параметр `id` на выборке будет иметь не значение `null` в случае пустой строки и значение записи коллекции из БД, а значение `idFlyOverPos`.
А сам `id` записи коллекции будет храниться в поле case class’a `idRecievTaskDet`
Это нужно учесть в:
- Структуре case class’а
- В переносе значений из записи из БД в case class для формирования `onRefresh`
- В условии по признаку пустая ли строка (теперь строка пустая, если `idRecievTaskDet === None.nl`):
```scala
override def beforeEdit(): Unit = {
if (getSelfVar("idRecievTaskDet").isNull) {
regRow()
}
}
```
- При сеттере в поле пустой строки в методе переноса умолчательных значений:
```scala
/**
* insertByParent(ropParent) + сеттеры заполненных полей case class'а Row
*
* @param ropParent
* @param row
* @return
*/
def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = {
insertByParent(ropParent) :/ { rop =>
//setter'ы для переноса умолчательных значений из Row
row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to(scala.collection.immutable.IndexedSeq))
.filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull)
.foreach(field => {
if (field._1 == sid.ns) {
setAttrValue(rop, sidFlyOverPos, field._2)
} else {
setAttrValue(rop, field._1, field._2)
}
})
rop
}
}
```
- При сеттере в поле пустой строки после создания записи проставить параметр выборки `idRecievTaskDet`, заместо `id`, в `id` только что созданной записи:
```scala
val rop = thisApi().registerByRow(thisRow())
setVar("idRecievTaskDet", rop.get(_.id))
```
- При взятии параметра у дочерних выборок по `super$id` -> `super$idRecievTaskDet`
- В `CWA` управление свойством `isEnabled` у операций:
```scala
selection.opers().setEnabled("Delete",selection.canDelete && selection.getSelfVar("idRecievTaskDet").notNull())
```
- Удаление:
```scala
thisApi().delete(thisApi().load(getSelfVar("idRecievTaskDet").asNLong))
```
- thisRop()
```scala
thisApi().load(getSelfVar("idRecievTaskDet").asJLong)
```
- `onInvalidateItem()`:
```scala
override protected def onInvalidateItem(): Unit = {
if (!getSelfVar(thisApi().sidRecievTaskDet).isNull && thisRop() != null) {
session.invalidateObject(thisRop())
}
}
```
- учесть в `onRefreshExt()` в тексте запроса
## Динамическое присоединение столбцов
Если такое отображение только для чтения и результат не использует значения, которые могут быть в кэше, тогда можно реализовать на реляционном запросе, иначе необходимо использовать инструменты `DynMetaBuilder` и `DynRecBuilder`.
### Реляционная реализация
В этом случае необходимо собрать текст запроса `selectStatement`.
Ниже пример формирования одного из столбца:
```scala
s"""
,(select string_agg(cast(t.id as varchar), ', ')
from Oil_Task t
where t.idResource = ${rv.idResource()}
and (t.dBegin < $sMainFromTab.dEndTime and t.dExec > $sMainFromTab.dBegTime)
and t.idObjectType = :${Oil_TaskMonitorPkg().sfltidObjectType}
and t.idStateMC >= 200
and t.idDepOwner = :super$$idGlobalDepOwner
group by t.idResource
) as \"$sNameFieldForGST[${rv.id()}]\"
"""
```
Пример текста после подстановки значения:
```{code}
(select string_agg(cast(t.id as varchar), ', ')
from Oil_Task t
where t.idResource = 115
and (t.dBegin < ‘2023-08-25 12:00:00’ and t.dExec > ‘2023-08-25 14:00:00’)
and t.idObjectType = 225
and t.idStateMC >= 200
and t.idDepOwner = 336
group by t.idResource
) as “idFlyOverWay[13]"
```
Мы получим столбец с названием `idFlyOverWay[13]`, результатом будет значение подзапроса.
### `DynMetaBuilder` и `DynRecBuilder`
Пример: `ru.bitec.app.oil.Oil_TaskMonitorAvi.List_CoreResource`.
Структура формирования:
```scala
Recs(<объекты, представляющие строку (список rop или case class)>)
.extend(.build())
.foreach((row, builder) => (row, .build))
```
- Пример использования `Recs`:
```scala
Recs(SomeApi().refreshByParent(ropMaster))
```
,где SomeApi() - какая-либо Api класса.
- Пример использования `.extend()`
Здесь формируются методанные о добавляемых столбцах: название столбца, тип данных, caption.
```scala
val dynMetaBuilder = DynMetaBuilder()
dynMetaBuilder.add("idFlyOverWay[1]", classOf[String], "ПЭ-1")
dynMetaBuilder.add("idFlyOverWay[20]", classOf[String], "ПЭ-2")
dynMetaBuilder.add("idFlyOverWay[360]", classOf[String], "ПЭ-3")
Recs(SomeApi().refreshByParent(ropMaster))
.extend(dynMetaBuilder.build())
```
,где SomeApi() - какая-либо Api класса.
- Пример использования .foreach()
Стоит понимать, что это не привычный обходчик по коллекции, который ничего не возвращает, а отдельный метод для динамического формирования столбцов, который должен ВЕРНУТЬ результат выборки!
В данном методе столбец получает построчно значения.
```scala
Recs(SomeApi().refreshByParent(ropMaster))
.extend(dynMetaBuilder.build())
.foreach((row, builder) => {
builder.set(("idFlyOverWay[1]", row.get(_.sDiscription)))
builder.set(("idFlyOverWay[20]", "hello, world"))
builder.set(("idFlyOverWay[20]", None.ns))
//метод должен вернуть кортеж
(row, builder.build)
})
```
,где SomeApi() - какая-либо Api класса.
Тут заданы различные значения для столбцов. На каждую строку `row` будет формироваться 3 значения для 3ёх столбцов. Столбец `idFlyOverWay[1]` будет в каждой строке разный, остальные же иметь одинаковое значение.
Описать столбец в разметке `avm` можно по имени поля без квадратных скобок.
## Динамическое изменение свойств avm
Для этого необходимо использовать следующий метод:
`ru.bitec.app.gtk.gl.Rep#setMetaProp`
[Описание](https://docs.global-system.ru/as/dev/reference/api/core-gtk/ru/bitec/gtk/core/gl/avi/CoreRepProxy.html#setmetaprop)
Пример использования:
```scala
setMetaProp(attr.sSystemName.get //для какого поля меняется свойство
//какое свойство
//в данном случае свойство задает системное имя атрибута в выборке,
//которое хранит настройки типа редактора
, s"View.Representation.Attributes.Attribute.Editor.editorTypeAttr"меняется
, thisPkg.getSEditorAttrName(attr) //значения свойства
)
```
## `OnrefreshExt` и значение даты с выборки
В `onRefreshExt` нет приведения `/*NDate*/`, нужно писать через `/*NString*/`.
В `onRefreshExt` в конструкции with объявляются поля, которые нужно получить с выборки (из кэша). Иногда требуется указать тип данных. Тип данных указывается в `/*...*/`, но парсер на знает `NDate`, поэтому необходимо указать, как `NString` и далее использовать в запросе `cast(... as timestamp)` или `as date`.
```scala
override protected def onRefreshExt: String = {
s"""with t as ( select
:id as id
,:idState as idState
,:gidSrc/*@NString*/ as gidSrc
,:idObjectType as idObjectType
,:idDepOwner as idDepOwner
,:idDischargePlace as idDischargePlace
,:idSegregationGroup as idSegregationGroup
,:idService as idService
,:idFlyOverWay as idFlyOverWay
,:idStateMC as idStateMC
,:dPlanEnd /*@NString*/ as dPlanEnd
,:dPlanBegin /*@NString*/ as dPlanBegin
)
SELECT
t.id
,to_char(cast(t.dPlanEnd as timestamp) - cast(t.dPlanBegin as timestamp), 'dd д. hh24 ч.') as nPlanBusy
,t1.sHeadLine_dz as idStateHL
,t2.sHeadLine_dz as idObjectTypeHL
,t3.sHeadLine_dz as idDepOwnerHL
,t4.sHeadLine_dz as idDischargePlaceHL
,t5.sMnemoCode_dz as idSegregationGroupHL
,t6.sHeadLine_dz as idServiceHL
,t7.sHeadLine as gidSrcHL
,t8.sHeadLine_dz as idFlyOverWayHL
FROM t
LEFT JOIN Btk_ClassState t1 on t.idState = t1.id
LEFT JOIN Btk_ObjectType t2 on t.idObjectType = t2.id
LEFT JOIN Bs_DepOwner t3 on t.idDepOwner = t3.id
LEFT JOIN Bs_Placement t4 on t.idDischargePlace = t4.id
LEFT JOIN Oil_SegregationGroup t5 on t.idSegregationGroup = t5.id
LEFT JOIN Gds_Service t6 on t.idService = t6.id
LEFT JOIN Btk_Object t7 on t.gidSrc = t7.gidRef
LEFT JOIN Oil_FlyOverWay t8 on t.idFlyOverWay = t8.id
"""
}
```
## Поиск отображения на выборке
`selection.form.findSelection(...)`
[Ссылка](https://docs.global-system.ru/as/dev/reference/api/core-gtk/ru/bitec/gtk/core/gl/CoreForm.html#findselection)
Полезный метод, который позволяет найти отображение по системному имени среди видимых в сессии.
Пример использования:
```scala
val sel = selection.form.findSelection(Bdg_ForecastAvi.card())
```
## Пользовательская блокировка
Пользовательская блокировка включается при взаимодействии пользователя с интерфейсом в методе `Dvi#beforeEdit`, который является результатом кодогенератора.
Принудительно можно включать блокировку с помощью вызова метода:
- `Btk_FormSessionApi().lockObject(gid)`
- `Btk_FormSessionApi().lockObjectMulti(gida)`
Больше про пользовательскую блокировку можно узнать [здесь](http://helpcenter.gs.local/btk_documentaion_v1/html/120_%D0%9F%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B0%D1%8F%20%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0.html).
## Объект класса в процессе создания и другие состояния объекта rop
Иногда, есть необходимость в Avi знать, что запись находится в процессе создания, т.е. не имеет реализации, как запись в таблице БД.
Это можно узнать по объекту записи `rop`:
- используйте метод Avi `thisRop()` чтобы получить объект `rop`, чья карточка открыта или на котором стоит фокус, если отображение `list`.
- если `rop.ropMode == InsertRopMode`, то объект находится в процессе создания, т.е. есть в кэше приложения, но не имеет реализации в таблице БД.
Где поле объекта `rop.ropMode` хранит в себе информацию состояния объекта. Есть и иные состояния объекта:
- ReadRopMode
- UpdateRopMode
- DeleteRopMode
- InsertRopMode
Пример кода из проекта:
```scala
//Документ должен быть закоммичен, чтобы были пройдены все соответсвующие проверки,
// т.к. по закрытию card_ReadFromFile будет flush()
if (rop.ropMode == InsertRopMode) {
throw AppException("Для вызова операции необходимо, чтобы документ был заполенен и сохранён.")
}
```
## Как узнать, что выборка является главным меню или главной выборкой формы
В системе есть 3 типа форм:
- главная
- модальная
- MDI (отображается в качестве закладки на главной форме)
У каждой формы есть главная выборка. Флаг `selection.isMainOnForm` и указывает, что выборка - главная на форме.
Условие `application.mainSelection == selection` определит, является ли выборка главным меню.